/**
 * Copyright(c) Live2D Inc. All rights reserved.
 *
 * Use of this source code is governed by the Live2D Open Software license
 * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */

#include "CubismMotion.hpp"

#include "CubismFramework.hpp"
#include "CubismMotionInternal.hpp"
#include "CubismMotionJson.hpp"
#include "CubismMotionQueueEntry.hpp"
#include "CubismMotionQueueManager.hpp"
#include "Id/CubismIdManager.hpp"
#include "Math/CubismMath.hpp"

#include <float.h>

namespace Live2D
{
    namespace Cubism
    {
        namespace Framework
        {

            namespace
            {

                const auto EffectNameEyeBlink = "EyeBlink";
                const auto EffectNameLipSync = "LipSync";
                const auto TargetNameModel = "Model";
                const auto TargetNameParameter = "Parameter";
                const auto TargetNamePartOpacity = "PartOpacity";

                CubismMotionPoint LerpPoints(const CubismMotionPoint a, const CubismMotionPoint b, const csmFloat32 t)
                {
                    CubismMotionPoint result;

                    result.Time = a.Time + ((b.Time - a.Time) * t);
                    result.Value = a.Value + ((b.Value - a.Value) * t);

                    return result;
                }

                csmFloat32 LinearEvaluate(const CubismMotionPoint *points, const csmFloat32 time)
                {
                    csmFloat32 t = (time - points[0].Time) / (points[1].Time - points[0].Time);

                    if (t < 0.0f)
                    {
                        t = 0.0f;
                    }

                    return points[0].Value + ((points[1].Value - points[0].Value) * t);
                }

                csmFloat32 BezierEvaluate(const CubismMotionPoint *points, const csmFloat32 time)
                {
                    csmFloat32 t = (time - points[0].Time) / (points[3].Time - points[0].Time);

                    if (t < 0.0f)
                    {
                        t = 0.0f;
                    }

                    const CubismMotionPoint p01 = LerpPoints(points[0], points[1], t);
                    const CubismMotionPoint p12 = LerpPoints(points[1], points[2], t);
                    const CubismMotionPoint p23 = LerpPoints(points[2], points[3], t);

                    const CubismMotionPoint p012 = LerpPoints(p01, p12, t);
                    const CubismMotionPoint p123 = LerpPoints(p12, p23, t);

                    return LerpPoints(p012, p123, t).Value;
                }

                csmFloat32 SteppedEvaluate(const CubismMotionPoint *points, const csmFloat32 time)
                {
                    Q_UNUSED(time)
                    return points[0].Value;
                }

                csmFloat32 InverseSteppedEvaluate(const CubismMotionPoint *points, const csmFloat32 time)
                {
                    Q_UNUSED(time)
                    return points[1].Value;
                }

                csmFloat32 EvaluateCurve(const CubismMotionData *motionData, const int index, csmFloat32 time)
                {
                    // Find segment to evaluate.
                    const CubismMotionCurve &curve = motionData->Curves[index];

                    int target = -1;
                    const int totalSegmentCount = curve.BaseSegmentIndex + curve.SegmentCount;
                    int pointPosition = 0;
                    for (int i = curve.BaseSegmentIndex; i < totalSegmentCount; ++i)
                    {
                        // Get first point of next segment.
                        pointPosition = motionData->Segments[i].BasePointIndex +
                                        (motionData->Segments[i].SegmentType == CubismMotionSegmentType_Bezier ? 3 : 1);

                        // Break if time lies within current segment.
                        if (motionData->Points[pointPosition].Time > time)
                        {
                            target = i;
                            break;
                        }
                    }

                    if (target == -1)
                    {
                        return motionData->Points[pointPosition].Value;
                    }

                    const CubismMotionSegment &segment = motionData->Segments[target];

                    return segment.Evaluate(&motionData->Points[segment.BasePointIndex], time);
                }

            } // namespace

            CubismMotion::CubismMotion()
                : _sourceFrameRate(30.0f), _loopDurationSeconds(-1.0f), _isLoop(false) // trueから false へデフォルトを変更
                  ,
                  _isLoopFadeIn(true) // ループ時にフェードインが有効かどうかのフラグ
                  ,
                  _lastWeight(0.0f), _motionData(NULL), _modelCurveIdEyeBlink(NULL), _modelCurveIdLipSync(NULL)
            {
            }

            CubismMotion::~CubismMotion()
            {
                CSM_DELETE(_motionData);
            }

            CubismMotion *CubismMotion::Create(const QByteArray &buffer, FinishedMotionCallback onFinishedMotionHandler)
            {
                CubismMotion *ret = CSM_NEW CubismMotion();

                ret->Parse(buffer);
                ret->_sourceFrameRate = ret->_motionData->Fps;
                ret->_loopDurationSeconds = ret->_motionData->Duration;
                ret->_onFinishedMotion = onFinishedMotionHandler;

                // NOTE: Editorではループありのモーション書き出しは非対応
                // ret->_loop = (ret->_motionData->Loop > 0);

                return ret;
            }

            csmFloat32 CubismMotion::GetDuration()
            {
                return _isLoop ? -1.0f : _loopDurationSeconds;
            }

            void CubismMotion::DoUpdateParameters(CubismModel *model, csmFloat32 userTimeSeconds, csmFloat32 fadeWeight,
                                                  CubismMotionQueueEntry *motionQueueEntry)
            {
                if (_modelCurveIdEyeBlink == NULL)
                {
                    _modelCurveIdEyeBlink = CubismFramework::GetIdManager()->GetId(EffectNameEyeBlink);
                }

                if (_modelCurveIdLipSync == NULL)
                {
                    _modelCurveIdLipSync = CubismFramework::GetIdManager()->GetId(EffectNameLipSync);
                }

                csmFloat32 timeOffsetSeconds = userTimeSeconds - motionQueueEntry->GetStartTime();

                if (timeOffsetSeconds < 0.0f)
                {
                    timeOffsetSeconds = 0.0f; // エラー回避
                }

                csmFloat32 lipSyncValue = FLT_MAX;
                csmFloat32 eyeBlinkValue = FLT_MAX;

                //まばたき、リップシンクのうちモーションの適用を検出するためのビット（maxFlagCount個まで
                const int MaxTargetSize = 64;
                csmUint64 lipSyncFlags = 0ULL;
                csmUint64 eyeBlinkFlags = 0ULL;

                //瞬き、リップシンクのターゲット数が上限を超えている場合
                if (_eyeBlinkParameterIds.size() > MaxTargetSize)
                {
                    CubismLogDebug("too many eye blink targets : %d", _eyeBlinkParameterIds.size());
                }
                if (_lipSyncParameterIds.size() > MaxTargetSize)
                {
                    CubismLogDebug("too many lip sync targets : %d", _lipSyncParameterIds.size());
                }

                const csmFloat32 tmpFadeIn =
                    (_fadeInSeconds <= 0.0f) ?
                        1.0f :
                        CubismMath::GetEasingSine((userTimeSeconds - motionQueueEntry->GetFadeInStartTime()) / _fadeInSeconds);

                const csmFloat32 tmpFadeOut =
                    (_fadeOutSeconds <= 0.0f || motionQueueEntry->GetEndTime() < 0.0f) ?
                        1.0f :
                        CubismMath::GetEasingSine((motionQueueEntry->GetEndTime() - userTimeSeconds) / _fadeOutSeconds);

                csmFloat32 value;
                int c, parameterIndex;

                // 'Repeat' time as necessary.
                csmFloat32 time = timeOffsetSeconds;

                if (_isLoop)
                {
                    while (time > _motionData->Duration)
                    {
                        time -= _motionData->Duration;
                    }
                }

                QVector<CubismMotionCurve> &curves = _motionData->Curves;

                // Evaluate model curves.
                for (c = 0; c < _motionData->CurveCount && curves[c].Type == CubismMotionCurveTarget_Model; ++c)
                {
                    // Evaluate curve and call handler.
                    value = EvaluateCurve(_motionData, c, time);

                    if (curves[c].Id == _modelCurveIdEyeBlink)
                    {
                        eyeBlinkValue = value;
                    }
                    else if (curves[c].Id == _modelCurveIdLipSync)
                    {
                        lipSyncValue = value;
                    }
                }

                int parameterMotionCurveCount = 0;

                for (; c < _motionData->CurveCount && curves[c].Type == CubismMotionCurveTarget_Parameter; ++c)
                {
                    parameterMotionCurveCount++;

                    // Find parameter index.
                    parameterIndex = model->GetParameterIndex(curves[c].Id);

                    // Skip curve evaluation if no value in sink.
                    if (parameterIndex == -1)
                    {
                        continue;
                    }

                    const csmFloat32 sourceValue = model->GetParameterValue(parameterIndex);

                    // Evaluate curve and apply value.
                    value = EvaluateCurve(_motionData, c, time);

                    if (eyeBlinkValue != FLT_MAX)
                    {
                        for (auto i = 0; i < _eyeBlinkParameterIds.size() && i < MaxTargetSize; ++i)
                        {
                            if (_eyeBlinkParameterIds[i] == curves[c].Id)
                            {
                                value *= eyeBlinkValue;
                                eyeBlinkFlags |= 1ULL << i;
                                break;
                            }
                        }
                    }

                    if (lipSyncValue != FLT_MAX)
                    {
                        for (auto i = 0; i < _lipSyncParameterIds.size() && i < MaxTargetSize; ++i)
                        {
                            if (_lipSyncParameterIds[i] == curves[c].Id)
                            {
                                value += lipSyncValue;
                                lipSyncFlags |= 1ULL << i;
                                break;
                            }
                        }
                    }

                    csmFloat32 v;
                    // パラメータごとのフェード
                    if (curves[c].FadeInTime < 0.0f && curves[c].FadeOutTime < 0.0f)
                    {
                        //モーションのフェードを適用
                        v = sourceValue + (value - sourceValue) * fadeWeight;
                    }
                    else
                    {
                        // パラメータに対してフェードインかフェードアウトが設定してある場合はそちらを適用
                        csmFloat32 fin;
                        csmFloat32 fout;

                        if (curves[c].FadeInTime < 0.0f)
                        {
                            fin = tmpFadeIn;
                        }
                        else
                        {
                            fin =
                                curves[c].FadeInTime == 0.0f ?
                                    1.0f :
                                    CubismMath::GetEasingSine((userTimeSeconds - motionQueueEntry->GetFadeInStartTime()) / curves[c].FadeInTime);
                        }

                        if (curves[c].FadeOutTime < 0.0f)
                        {
                            fout = tmpFadeOut;
                        }
                        else
                        {
                            fout = (curves[c].FadeOutTime == 0.0f || motionQueueEntry->GetEndTime() < 0.0f) ?
                                       1.0f :
                                       CubismMath::GetEasingSine((motionQueueEntry->GetEndTime() - userTimeSeconds) / curves[c].FadeOutTime);
                        }

                        const csmFloat32 paramWeight = _weight * fin * fout;

                        // パラメータごとのフェードを適用
                        v = sourceValue + (value - sourceValue) * paramWeight;
                    }

                    model->SetParameterValue(parameterIndex, v);
                }

                {
                    if (eyeBlinkValue != FLT_MAX)
                    {
                        for (auto i = 0; i < _eyeBlinkParameterIds.size() && i < MaxTargetSize; ++i)
                        {
                            const csmFloat32 sourceValue = model->GetParameterValue(_eyeBlinkParameterIds[i]);
                            //モーションでの上書きがあった時にはまばたきは適用しない
                            if ((eyeBlinkFlags >> i) & 0x01)
                            {
                                continue;
                            }

                            const csmFloat32 v = sourceValue + (eyeBlinkValue - sourceValue) * fadeWeight;

                            model->SetParameterValue(_eyeBlinkParameterIds[i], v);
                        }
                    }

                    if (lipSyncValue != FLT_MAX)
                    {
                        for (auto i = 0; i < _lipSyncParameterIds.size() && i < MaxTargetSize; ++i)
                        {
                            const csmFloat32 sourceValue = model->GetParameterValue(_lipSyncParameterIds[i]);
                            //モーションでの上書きがあった時にはリップシンクは適用しない
                            if ((lipSyncFlags >> i) & 0x01)
                            {
                                continue;
                            }

                            const csmFloat32 v = sourceValue + (lipSyncValue - sourceValue) * fadeWeight;

                            model->SetParameterValue(_lipSyncParameterIds[i], v);
                        }
                    }
                }

                for (; c < _motionData->CurveCount && curves[c].Type == CubismMotionCurveTarget_PartOpacity; ++c)
                {
                    // Find parameter index.
                    parameterIndex = model->GetParameterIndex(curves[c].Id);

                    // Skip curve evaluation if no value in sink.
                    if (parameterIndex == -1)
                    {
                        continue;
                    }

                    // Evaluate curve and apply value.
                    value = EvaluateCurve(_motionData, c, time);

                    model->SetParameterValue(parameterIndex, value);
                }

                if (timeOffsetSeconds >= _motionData->Duration)
                {
                    if (_isLoop)
                    {
                        motionQueueEntry->SetStartTime(userTimeSeconds); //最初の状態へ
                        if (_isLoopFadeIn)
                        {
                            //ループ中でループ用フェードインが有効のときは、フェードイン設定し直し
                            motionQueueEntry->SetFadeInStartTime(userTimeSeconds);
                        }
                    }
                    else
                    {
                        if (this->_onFinishedMotion != NULL)
                        {
                            this->_onFinishedMotion(this);
                        }

                        motionQueueEntry->IsFinished(true);
                    }
                }

                _lastWeight = fadeWeight;
            }

            void CubismMotion::Parse(const QByteArray &buffer)
            {
                _motionData = CSM_NEW CubismMotionData;

                CubismMotionJson json(buffer);

                _motionData->Duration = json.GetMotionDuration();
                _motionData->Loop = json.IsMotionLoop();
                _motionData->CurveCount = json.GetMotionCurveCount();
                _motionData->Fps = json.GetMotionFps();
                _motionData->EventCount = json.GetEventCount();

                if (json.IsExistMotionFadeInTime())
                {
                    _fadeInSeconds = (json.GetMotionFadeInTime() < 0.0f) ? 1.0f : json.GetMotionFadeInTime();
                }
                else
                {
                    _fadeInSeconds = 1.0f;
                }

                if (json.IsExistMotionFadeOutTime())
                {
                    _fadeOutSeconds = (json.GetMotionFadeOutTime() < 0.0f) ? 1.0f : json.GetMotionFadeOutTime();
                }
                else
                {
                    _fadeOutSeconds = 1.0f;
                }

                _motionData->Curves.resize(_motionData->CurveCount);             //, CubismMotionCurve(), true);
                _motionData->Segments.resize(json.GetMotionTotalSegmentCount()); //, CubismMotionSegment(), true);
                _motionData->Points.resize(json.GetMotionTotalPointCount());     //, CubismMotionPoint(), true);
                _motionData->Events.resize(_motionData->EventCount);             //, CubismMotionEvent(), true);

                int totalPointCount = 0;
                int totalSegmentCount = 0;

                // Curves
                for (int curveCount = 0; curveCount < _motionData->CurveCount; ++curveCount)
                {
                    if (json.GetMotionCurveTarget(curveCount) == TargetNameModel)
                    {
                        _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_Model;
                    }
                    else if (json.GetMotionCurveTarget(curveCount) == TargetNameParameter)
                    {
                        _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_Parameter;
                    }
                    else if (json.GetMotionCurveTarget(curveCount) == TargetNamePartOpacity)
                    {
                        _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_PartOpacity;
                    }

                    _motionData->Curves[curveCount].Id = json.GetMotionCurveId(curveCount);

                    _motionData->Curves[curveCount].BaseSegmentIndex = totalSegmentCount;

                    _motionData->Curves[curveCount].FadeInTime =
                        (json.IsExistMotionCurveFadeInTime(curveCount)) ? json.GetMotionCurveFadeInTime(curveCount) : -1.0f;
                    _motionData->Curves[curveCount].FadeOutTime =
                        (json.IsExistMotionCurveFadeOutTime(curveCount)) ? json.GetMotionCurveFadeOutTime(curveCount) : -1.0f;

                    // Segments
                    for (int segmentPosition = 0; segmentPosition < json.GetMotionCurveSegmentCount(curveCount);)
                    {
                        if (segmentPosition == 0)
                        {
                            _motionData->Segments[totalSegmentCount].BasePointIndex = totalPointCount;

                            _motionData->Points[totalPointCount].Time = json.GetMotionCurveSegment(curveCount, segmentPosition);
                            _motionData->Points[totalPointCount].Value = json.GetMotionCurveSegment(curveCount, segmentPosition + 1);

                            totalPointCount += 1;
                            segmentPosition += 2;
                        }
                        else
                        {
                            _motionData->Segments[totalSegmentCount].BasePointIndex = totalPointCount - 1;
                        }

                        const int segment = static_cast<int>(json.GetMotionCurveSegment(curveCount, segmentPosition));

                        switch (segment)
                        {
                            case CubismMotionSegmentType_Linear:
                            {
                                _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Linear;
                                _motionData->Segments[totalSegmentCount].Evaluate = LinearEvaluate;

                                _motionData->Points[totalPointCount].Time = json.GetMotionCurveSegment(curveCount, (segmentPosition + 1));
                                _motionData->Points[totalPointCount].Value = json.GetMotionCurveSegment(curveCount, (segmentPosition + 2));

                                totalPointCount += 1;
                                segmentPosition += 3;

                                break;
                            }
                            case CubismMotionSegmentType_Bezier:
                            {
                                _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Bezier;
                                _motionData->Segments[totalSegmentCount].Evaluate = BezierEvaluate;

                                _motionData->Points[totalPointCount].Time = json.GetMotionCurveSegment(curveCount, (segmentPosition + 1));
                                _motionData->Points[totalPointCount].Value = json.GetMotionCurveSegment(curveCount, (segmentPosition + 2));

                                _motionData->Points[totalPointCount + 1].Time = json.GetMotionCurveSegment(curveCount, (segmentPosition + 3));
                                _motionData->Points[totalPointCount + 1].Value = json.GetMotionCurveSegment(curveCount, (segmentPosition + 4));

                                _motionData->Points[totalPointCount + 2].Time = json.GetMotionCurveSegment(curveCount, (segmentPosition + 5));
                                _motionData->Points[totalPointCount + 2].Value = json.GetMotionCurveSegment(curveCount, (segmentPosition + 6));

                                totalPointCount += 3;
                                segmentPosition += 7;

                                break;
                            }
                            case CubismMotionSegmentType_Stepped:
                            {
                                _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Stepped;
                                _motionData->Segments[totalSegmentCount].Evaluate = SteppedEvaluate;

                                _motionData->Points[totalPointCount].Time = json.GetMotionCurveSegment(curveCount, (segmentPosition + 1));
                                _motionData->Points[totalPointCount].Value = json.GetMotionCurveSegment(curveCount, (segmentPosition + 2));

                                totalPointCount += 1;
                                segmentPosition += 3;

                                break;
                            }
                            case CubismMotionSegmentType_InverseStepped:
                            {
                                _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_InverseStepped;
                                _motionData->Segments[totalSegmentCount].Evaluate = InverseSteppedEvaluate;

                                _motionData->Points[totalPointCount].Time = json.GetMotionCurveSegment(curveCount, (segmentPosition + 1));
                                _motionData->Points[totalPointCount].Value = json.GetMotionCurveSegment(curveCount, (segmentPosition + 2));

                                totalPointCount += 1;
                                segmentPosition += 3;

                                break;
                            }
                            default:
                            {
                                CSM_ASSERT(0);
                                break;
                            }
                        }

                        ++_motionData->Curves[curveCount].SegmentCount;
                        ++totalSegmentCount;
                    }
                }

                for (int userdatacount = 0; userdatacount < json.GetEventCount(); ++userdatacount)
                {
                    _motionData->Events[userdatacount].FireTime = json.GetEventTime(userdatacount);
                    _motionData->Events[userdatacount].Value = json.GetEventValue(userdatacount);
                }
            }

            void CubismMotion::SetParameterFadeInTime(CubismIdHandle parameterId, csmFloat32 value)
            {
                QVector<CubismMotionCurve> &curves = _motionData->Curves;

                for (csmInt16 i = 0; i < _motionData->CurveCount; ++i)
                {
                    if (parameterId == curves[i].Id)
                    {
                        curves[i].FadeInTime = value;
                        return;
                    }
                }
            }

            void CubismMotion::SetParameterFadeOutTime(CubismIdHandle parameterId, csmFloat32 value)
            {
                QVector<CubismMotionCurve> &curves = _motionData->Curves;

                for (csmInt16 i = 0; i < _motionData->CurveCount; ++i)
                {
                    if (parameterId == curves[i].Id)
                    {
                        curves[i].FadeOutTime = value;
                        return;
                    }
                }
            }

            csmFloat32 CubismMotion::GetParameterFadeInTime(CubismIdHandle parameterId) const
            {
                QVector<CubismMotionCurve> &curves = _motionData->Curves;

                for (csmInt16 i = 0; i < _motionData->CurveCount; ++i)
                {
                    if (parameterId == curves[i].Id)
                    {
                        return curves[i].FadeInTime;
                    }
                }

                return -1;
            }

            csmFloat32 CubismMotion::GetParameterFadeOutTime(CubismIdHandle parameterId) const
            {
                QVector<CubismMotionCurve> &curves = _motionData->Curves;

                for (csmInt16 i = 0; i < _motionData->CurveCount; ++i)
                {
                    if (parameterId == curves[i].Id)
                    {
                        return curves[i].FadeOutTime;
                    }
                }

                return -1;
            }

            void CubismMotion::IsLoop(bool loop)
            {
                this->_isLoop = loop;
            }

            bool CubismMotion::IsLoop() const
            {
                return this->_isLoop;
            }

            void CubismMotion::IsLoopFadeIn(bool loopFadeIn)
            {
                this->_isLoopFadeIn = loopFadeIn;
            }

            bool CubismMotion::IsLoopFadeIn() const
            {
                return this->_isLoopFadeIn;
            }

            csmFloat32 CubismMotion::GetLoopDuration()
            {
                return _loopDurationSeconds;
            }

            void CubismMotion::SetEffectIds(const QVector<CubismIdHandle> &eyeBlinkParameterIds,
                                            const QVector<CubismIdHandle> &lipSyncParameterIds)
            {
                _eyeBlinkParameterIds = eyeBlinkParameterIds;
                _lipSyncParameterIds = lipSyncParameterIds;
            }

            const QVector<const QString *> &CubismMotion::GetFiredEvent(csmFloat32 beforeCheckTimeSeconds, csmFloat32 motionTimeSeconds)
            {
                /// イベントの発火チェック
                for (int u = 0; u < _motionData->EventCount; ++u)
                {
                    if ((_motionData->Events[u].FireTime > beforeCheckTimeSeconds) && (_motionData->Events[u].FireTime <= motionTimeSeconds))
                    {
                        _firedEventValues.append(&_motionData->Events[u].Value);
                    }
                }

                return _firedEventValues;
            }

        } // namespace Framework
    }     // namespace Cubism
} // namespace Live2D
